package edu.amm.neur;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.ResourceBundle;

import ru.vsu.amm.algebra.Matrix;
import ru.vsu.amm.io.MatrixIO;

import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Label;
import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.FileChooser;
import javafx.stage.FileChooser.ExtensionFilter;
import javafx.stage.Window;

/**
 *
 * @author Илья
 */
public class RecognizerController implements Initializable {
	
	private Perceptron[] perceptrons = new Perceptron[MAX_NEURON_COUNT];
	
	private Canvas canvas;
	private GraphicsContext context;
	
	private int counter;
	private int[][] points;
	
	private boolean isPressed;
	
	private FileChooser fileChooser = new FileChooser();
	
	@FXML
	private Pane canvasPane;
	@FXML
	private ChoiceBox<Integer> neuronCount;
	@FXML
	private ChoiceBox<String> imageType;
	@FXML
	private CheckBox showScaled;
	@FXML
	private Label info;
	@FXML
	private Button train;
	@FXML
	private Button recognize;
	@FXML
	private Button clear;
	@FXML
	private Button save;
	@FXML
	private Button load;
	@FXML
	private Button exit;
	
	@Override
	public void initialize(URL location, ResourceBundle resources) {
		initDrawing();
		initContent();
		setHandlers();
		initFileChooser();
	}
	
	private void initDrawing() {
		canvas = new Canvas(WIDTH, HEIGHT);
		context = canvas.getGraphicsContext2D();
		context.setStroke(LINE_COLOR);
		context.setLineWidth(2);
		canvasPane.getChildren().add(canvas);
		
		clear();
		
		points = new int[PATH_POINTS_COUNT][];
		for (int i = 0; i < points.length; i++)
			points[i] = new int[2];
	}
	
	private void initContent() {
		neuronCount.getItems().clear();
		for (int n = 1; n <= MAX_NEURON_COUNT; n++) {
			perceptrons[n - 1] = new Perceptron(INPUT_LENGTH, n);
			neuronCount.getItems().add(n);
		}
		neuronCount.getSelectionModel().select(0);
		populateImageTypes(neuronCount.getSelectionModel().getSelectedItem());
		clearInfo();
	}
	
	private void setHandlers() {
		canvas.setOnMousePressed(new EventHandler<MouseEvent>() {
			@Override
			public void handle(MouseEvent event) {
				clearInfo();
				pressed(event.getX(), event.getY());
			}
		});
		
		canvas.setOnMouseDragged(new EventHandler<MouseEvent>() {
			@Override
			public void handle(MouseEvent event) {
				dragged(event.getX(), event.getY());
			}
		});
		
		canvas.setOnMouseReleased(new EventHandler<MouseEvent>() {
			@Override
			public void handle(MouseEvent event) {
				released();
			}
		});
		
		neuronCount.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Integer>() {
			public void changed(ObservableValue<? extends Integer> observable,
				Integer oldValue, Integer newValue) {
				clearInfo();
				populateImageTypes(newValue);
			}
		});
		
		train.setOnAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(ActionEvent event) {
				clearInfo();
				train();
			}
		});
		
		recognize.setOnAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(ActionEvent event) {
				clearInfo();
				recognize();
			}
		});
		
		clear.setOnAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(ActionEvent event) {
				clearInfo();
				clear();
			}
		});
		
		save.setOnAction(new EventHandler<ActionEvent>() {
			public void handle(ActionEvent event) {
				clearInfo();
				save(((Button) event.getSource()).getScene().getWindow());
			}
		});
		
		load.setOnAction(new EventHandler<ActionEvent>() {
			public void handle(ActionEvent event) {
				clearInfo();
				load(((Button) event.getSource()).getScene().getWindow());
			}
		});
		
		exit.setOnAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(ActionEvent event) {
				exit();
			}
		});
	}
	
	private void initFileChooser() {
		try {
			fileChooser.setInitialDirectory(new File("./").getCanonicalFile());
		} catch (IOException e) {}
		
		fileChooser.getExtensionFilters().add(new ExtensionFilter("Текстовый файл", "*.txt"));
	}
	
	private void populateImageTypes(int neuronCount) {
		String[] labels = Arrays.copyOf(LABELS, neuronCount == 1 ? 2 : neuronCount);
		imageType.getItems().clear();
		imageType.getItems().addAll(labels);
		imageType.getSelectionModel().select(0);
	}
	
	private void pressed(double x, double y) {
		isPressed = true;
		counter = -1;
		dragged(x, y);
	}
	
	private void dragged(double x, double y) {
		if (!isPressed)
			return;
		
		int i = (int) x;
		int j = (int) y;
		if (i < 0 || i >= WIDTH || j < 0 || j >= HEIGHT) {
			counter = -1;
			return;
		}
		
		counter++;
		int modulo = counter % points.length;
		
		points[modulo][0] = i;
		points[modulo][1] = j;
		
		int n = points.length - 1;
		if (modulo == n) {
			drawCurve();
			points[0][0] = points[n][0];
			points[0][1] = points[n][1];
			counter++;
		}
	}
	
	private void drawCurve() {
		context.beginPath();
		context.moveTo(points[0][0], points[0][1]);
		context.bezierCurveTo(points[1][0], points[1][1], points[2][0], points[2][1], points[3][0], points[3][1]);
		context.stroke();
	}
	
	private void released() {
		isPressed = false;
	}
	
	private void clear() {
		GraphicsUtils.clear(canvas, BACK_COLOR);
	}
	
	private void train() {
		boolean[] digitized = getDigitized();
		Perceptron perceptron = getPerceptron();
		boolean[] desiredOutput = perceptron.getVectorForImageType(getImageTypeIndex());
		try {
			perceptron.addTrainigPair(digitized, desiredOutput);
			showInfo("Обучение завершено");
		} catch (RuntimeException e) {
			showError("Возникла коллизия при обучении!");
		}
	}
	
	private void recognize() {
		boolean[] digitized = getDigitized();
		Perceptron perceptron = getPerceptron(); 
		boolean[] recognized = perceptron.recognize(digitized);
		
		int imageTypeIndex = perceptron.getTypeIndexForVector(recognized);
		if (imageTypeIndex < 0)
			showError(String.format("Невозможно распознать тип изображения. Выходной вектор: %s", toString(recognized)));
		else
			showInfo(String.format("Распознанный тип изображения: %s", LABELS[imageTypeIndex]));
	}
	
	private void clearInfo() {
		info.setText("");
	}
	
	private void showInfo(String text) {
		info.setTextFill(INFO_COLOR);
		info.setText(text);
	}
	
	private void showError(String text) {
		info.setTextFill(ERROR_COLOR);
		info.setText(text);
	}
	
	private void save(Window window) {
		File file = fileChooser.showSaveDialog(window);
		if (file == null)
			return;
		
		try {
			MatrixIO.writeToFile(getPerceptron().getWeights(), file.getCanonicalPath());
			showInfo("Матрица весов сохранена!");
		} catch (IOException e) {
			showError("Невозможно сохранить файл");
		}
	}
	
	private void load(Window window) {
		File file = fileChooser.showOpenDialog(window);
		if (file == null)
			return;
		
		try {
			Matrix A = MatrixIO.readFromFile(file.getCanonicalPath());
			for (int i = 0; i < perceptrons.length; i++)
				if (A.rows() == perceptrons[i].getWeights().rows()
					&& A.columns() == perceptrons[i].getWeights().columns()) {
					
					perceptrons[i] = new Perceptron(A);
					neuronCount.getSelectionModel().select(i);
					populateImageTypes(getNeuronCount());
					
					showInfo("Матрица весов загружена!");
					return;
				}
			
			showError("Матрица весов некорректна");
		} catch (IOException e) {
			showError("Невозможно прочитать файл");
		}
	}
	
	private void exit() {
		Platform.exit();
	}
	
	private boolean[] getDigitized() {
		WritableImage image = canvas.snapshot(null, null);
		GraphicsUtils.maximize(image, LINE_COLOR);
		
		if (showScaled.isSelected())
			GraphicsUtils.draw(canvas, image, BACK_COLOR);
		
		return GraphicsUtils.digitize(image, LINE_COLOR);
	}
	
	private Perceptron getPerceptron() {
		return perceptrons[getNeuronCount() - 1];
	}
	
	private int getNeuronCount() {
		return neuronCount.getSelectionModel().getSelectedItem();
	}
	
	private int getImageTypeIndex() {
		return imageType.getSelectionModel().getSelectedIndex();
	}
	
	private static final int MAX_NEURON_COUNT = 3;
	private static final String[] LABELS = {
		"O",
		"X",
		"+"
	};
	
	private static final int WIDTH = 200;
	private static final int HEIGHT = 200;
	
	private static final int INPUT_LENGTH = GraphicsUtils.getDigitizedVectorLength(WIDTH, HEIGHT);
	
	private static final int PATH_POINTS_COUNT = 4;
	
	private static final Color LINE_COLOR = Color.BLACK;
	private static final Color BACK_COLOR = Color.WHITE;
	private static final Color INFO_COLOR = Color.GREEN;
	private static final Color ERROR_COLOR = Color.RED;
	
	private static String toString(boolean[] vector) {
		StringBuffer buffer = new StringBuffer("(");
		for (int i = 0; i < vector.length; i++) {
			buffer.append(vector[i] ? '1' : '0');
			if (i < vector.length - 1)
				buffer.append(", ");
			else
				buffer.append(')');
		}
		
		return buffer.toString();
	}
}